﻿using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
using System.ComponentModel;
using System.Net;
using System.Security.Principal;
using System.Threading.Tasks;

namespace Zhaoxi.OpcUA.Lesson
{
    internal class Program
    {
        static async Task Main(string[] args)
        {

            #region OPCFoundation.NetStandard.Opc.Ua库
            // 作为一个客户端,需要连接到某个服务端
            // ApplicationConfiguration configuration,         // 客户端应用配置信息
            // ConfiguredEndpoint endpoint,                    // IP:Port 
            // bool updateBeforeConnect,                       // 在连接之前更新，服务节点描述
            // string sessionName,                             // 会话名称
            // uint sessionTimeout,                            // 会话超时时间
            // IUserIdentity identity,                         // 用户名和密码
            // IList<string> preferredLocales                  // 与会话关联的用户身份

            // 匿名连接-->适合封闭的局域网
            // Session session = await AnonymousConnect();

            // 用户名密码连接
            //Session session = await UserNameConnect();

            // 认证连接--手动、自动
            //Session session = await CertificateConnect();

            // 获取服务器中的节点列表
            //BrowserNodes(session);

            // 读 (同步、异步、订阅)
            // 同步读
            //UARead(session);

            // 异步读
            //UAAsyncRead(session);
            // 业务层面异步测试 callback委托回调来获取返回值
            //List<RequestNode> nodes = new List<RequestNode>();
            //nodes.Add(new RequestNode() { NodeId = "ns=2;s=通道 1.设备 1.标记 2" });
            //nodes.Add(new RequestNode() { NodeId = "ns=2;s=数据类型示例.16 位设备.R 寄存器.Word4" });
            //// 异步读委托回调的方式处理返回值
            //UAAsyncRead(session, nodes, () =>
            //{
            //    foreach (RequestNode node in nodes)
            //    {
            //        Console.WriteLine($"{node.NodeId}:{node.Value}");
            //    }
            //});

            // 异步读Task的方式处理返回值
            //await UAAsyncTaskRead(session, nodes);
            //foreach (RequestNode node in nodes)
            //{
            //    Console.WriteLine($"{node.NodeId}:{node.Value}");
            //}

            // 订阅模式
            //Subscription(session);

            // 写(同步、异步)
            // 同步写入
            //Write(session);

            // 异步写入
            //AsyncWrite(session);

            // 异步写入Task方式
            //await AsyncTaskWrite(session);
            #endregion

            Console.ReadKey();
        }

        #region 创建OPC UA服务器连接
        /// <summary>
        /// 匿名访问OPC UA服务端
        /// </summary>
        /// <returns></returns>
        private static Task<Session> AnonymousConnect()
        {
            ApplicationConfiguration configuration = new ApplicationConfiguration();
            configuration.ClientConfiguration = new ClientConfiguration();
            // 这里需要进行TCP的三次握手
            EndpointDescription description = CoreClientUtils.SelectEndpoint(configuration, discoveryUrl: "opc.tcp://127.0.0.1:49320", useSecurity: false);
            ConfiguredEndpoint endpoint = new ConfiguredEndpoint(null, description);

            return Session.Create(
                configuration,
                endpoint,
                false,
                "Zhaoxi OPC",
                6000,
                new UserIdentity(),
                new string[] { }
            );
        }

        /// <summary>
        /// 用户名密码访问OPC UA服务端
        /// </summary>
        /// <returns></returns>
        private static Task<Session> UserNameConnect()
        {
            #region 客户端应用连接配置信息
            ApplicationConfiguration configuration = new ApplicationConfiguration();
            // 当创建连接对象时，必须给configuration的ClientConfiguration属性进行初始化
            configuration.ClientConfiguration = new ClientConfiguration();
            // 配置configuration对象的 CertificateValidation属性
            CertificateValidator validator = new CertificateValidator();
            // 针对登录过程中的认证处理
            validator.CertificateValidation += (se, ev) =>
            {
                // 把不信任认证拦截
                if (ev.Error.StatusCode.Code == StatusCodes.BadCertificateUntrusted)
                {
                    ev.Accept = true;  // 放行,即使证书不受信任也继续连接
                }

                // 如果其他的认证结果，继续单独处理
            };
            // 拒绝SHA1签名的证书
            validator.RejectSHA1SignedCertificates = false;
            configuration.CertificateValidator = validator;

            #endregion

            #region 连接地址
            // 这里需要进行TCP的三次握手
            EndpointDescription description = CoreClientUtils.SelectEndpoint(configuration, discoveryUrl: "opc.tcp://127.0.0.1:49320", useSecurity: false);
            ConfiguredEndpoint endpoint = new ConfiguredEndpoint(null, description);

            // 创建一个操作对象
            return Session.Create(
               configuration,
               endpoint,
               false,
               "Zhaoxi OPC",
               6000,
               new UserIdentity("Zhaoxi", "123456"),
               new string[] { }
            );
            #endregion

        }

        /// <summary>
        /// 证书方式连接OPC UA服务端
        /// </summary>
        /// <returns></returns>
        private static async Task<Session> CertificateConnect()
        {
            #region 客户端应用连接配置信息
            ApplicationConfiguration configuration = new ApplicationConfiguration();
            // 当创建连接对象时，必须给configuration的ClientConfiguration属性进行初始化
            configuration.ClientConfiguration = new ClientConfiguration();
            // 配置configuration对象的 CertificateValidation属性
            CertificateValidator validator = new CertificateValidator();
            // 针对登录过程中的认证处理
            validator.CertificateValidation += (se, ev) =>
            {
                // 把不信任认证拦截
                if (ev.Error.StatusCode.Code == StatusCodes.BadCertificateUntrusted)
                {
                    ev.Accept = true;  // 放行,即使证书不受信任也继续连接
                }

                // 如果其他的认证结果，继续单独处理
            };
            // 拒绝SHA1签名的证书
            //validator.RejectSHA1SignedCertificates = false;

            configuration.CertificateValidator = validator;

            #endregion

            #region 认证连接配置
            string appName = "opc_test";
            // 客户端名称
            configuration.ApplicationName = appName;
            // 认证URI
            configuration.ApplicationUri = $"urn:{Dns.GetHostName()}:{appName}";
            // 安全配置
            configuration.SecurityConfiguration = new SecurityConfiguration()
            {
                RejectSHA1SignedCertificates = false, // 是否拒绝SHA1签名的证书
                MinimumCertificateKeySize = 1024, // 最小证书密钥大小
                ApplicationCertificate = new CertificateIdentifier
                {
                    StoreType = CertificateStoreType.X509Store,
                    StorePath = $"CurrentUser\\" + appName,
                    SubjectName = appName
                }
            };
            // 客户端配置信息验证
            await configuration.Validate(ApplicationType.Client);

            ApplicationInstance applicationInstance = new ApplicationInstance();
            applicationInstance.ApplicationConfiguration = configuration;
            // 生成证书并提交到服务器进行验证
            bool state = await applicationInstance.CheckApplicationInstanceCertificate(false, 1024);
            #endregion

            #region 连接地址
            // 这里需要进行TCP的三次握手
            EndpointDescription description = CoreClientUtils.SelectEndpoint(configuration, discoveryUrl: "opc.tcp://127.0.0.1:49320", useSecurity: false);
            ConfiguredEndpoint endpoint = new ConfiguredEndpoint(null, description);

            // 创建一个操作对象
            return await Session.Create(
               configuration,
               endpoint,
               false,
               "Zhaoxi OPC",
               6000,
               new UserIdentity("Zhaoxi", "123456"),
               new string[] { }
            );
            #endregion
        }
        #endregion

        #region 浏览节点
        // 浏览节点的目的就是为了可以做节点浏览配置
        private static void BrowserNodes(Session session)
        {
            // 使用Browser对象进行浏览
            Browser browser = new Browser(session);
            // 关于NodeId:格式有要求
            // 固定格式:
            // ns -> namespace XML格式结构
            // s -> string
            //ReferenceDescriptionCollection collection = browser.Browse(new NodeId("ns=2;s=通道 1")); // 通道 1.设备 1.标记 1
            //ReferenceDescriptionCollection collection = browser.Browse("ns=0;i=84");
            ReferenceDescriptionCollection collection = browser.Browse(ObjectIds.RootFolder); // ObjectIds.RootFolder 等同于 ns=0;i=84
            foreach (var item in collection)
            {
                Console.WriteLine($"节点名称:{item.DisplayName},节点ID:{item.NodeId}");
            }

            // 如果需要所有分支,需要利用递归的方式进行处理

        }
        #endregion

        #region UA 读取
        /// <summary>
        /// 同步读取
        /// </summary>
        /// <param name="session"></param>
        private static void UARead(Session session)
        {
            //RequestHeader requestHeader,  // 请求头
            //double maxAge,    // 毫秒,1000代表1秒钟前的数据,设置为0表示当前数据
            //TimestampsToReturn timestampsToReturn,  // 返回结果 (两个：服务端/客户端) 的时间戳
            //ReadValueIdCollection nodesToRead,      // 需要请求的Node集合
            //out DataValueCollection results,        // 返回对应的Node的值集合
            //out DiagnosticInfoCollection diagnosticInfos  // 诊断信息集合

            ReadValueIdCollection readValueIds = new ReadValueIdCollection();
            ReadValueId readValueId = new ReadValueId()
            {
                NodeId = "ns=2;s=通道 1.设备 1.标记 2",    // 值
                AttributeId = Attributes.Value,
            };
            readValueIds.Add(readValueId);

            readValueId = new ReadValueId()
            {
                NodeId = "ns=2;s=数据类型示例.16 位设备.R 寄存器.Word4",  // 值
                AttributeId = Attributes.Value,
            };
            readValueIds.Add(readValueId);

            readValueId = new ReadValueId()
            {
                NodeId = "ns=2;s=数据类型示例.16 位设备.R 寄存器.WordArray",  // 集合
                AttributeId = Attributes.Value,
            };
            readValueIds.Add(readValueId);

            session.Read(
                new RequestHeader(),
                0,
                TimestampsToReturn.Both, // 返回结果的时间戳 (Both:服务端和客户端的时间戳)TimestampsToReturn.Server(服务端), TimestampsToReturn.Source(客户端)
                readValueIds,
                out DataValueCollection results,
                out DiagnosticInfoCollection diagnosticInfos
                );

            foreach (DataValue item in results)
            {
                // item.WrappedValue.TypeInfo.ValueRank 结果为 -1：针对值结果，1：针对集合结果
                if (item.WrappedValue.TypeInfo.ValueRank == -1) // 针对值结果
                {
                    Console.WriteLine(item.Value);
                }
                else if (item.WrappedValue.TypeInfo.ValueRank == 1)  // 针对集合结果
                {
                    if (item.WrappedValue.TypeInfo.BuiltInType == BuiltInType.UInt16)
                    {
                        Console.WriteLine("数据类型示例.16 位设备.R 寄存器.WordArray:");
                        ushort[] us = (ushort[])item.Value;
                        foreach (ushort u in us)
                        {
                            Console.WriteLine(u);
                        }
                    }
                }

                //if (item.Value is ushort[] us)
                //{
                //    Console.WriteLine("数据类型示例.16 位设备.R 寄存器.WordArray:");
                //    foreach (var uVal in us)
                //    {
                //        Console.WriteLine(uVal);
                //    }
                //}
                //else
                //{
                //    Console.WriteLine(item.Value);
                //}
            }

        }

        /// <summary>
        /// 异步读取
        /// </summary>
        /// <param name="session"></param>
        private static void UAAsyncRead(Session session)
        {
            ReadValueIdCollection readValueIds = new ReadValueIdCollection();
            ReadValueId readValueId = new ReadValueId()
            {
                NodeId = "ns=2;s=通道 1.设备 1.标记 2",    // 值
                AttributeId = Attributes.Value,
            };
            readValueIds.Add(readValueId);

            readValueId = new ReadValueId()
            {
                NodeId = "ns=2;s=数据类型示例.16 位设备.R 寄存器.Word4",  // 值
                AttributeId = Attributes.Value,
            };
            readValueIds.Add(readValueId);

            readValueId = new ReadValueId()
            {
                NodeId = "ns=2;s=数据类型示例.16 位设备.R 寄存器.WordArray",  // 集合
                AttributeId = Attributes.Value,
            };
            readValueIds.Add(readValueId);

            //RequestHeader requestHeader,  // 请求头
            //double maxAge,    // 毫秒,1000代表1秒钟前的数据,设置为0表示当前数据
            //TimestampsToReturn timestampsToReturn,  // 返回结果 (两个：服务端/客户端) 的时间戳
            //ReadValueIdCollection nodesToRead,      // 需要请求的Node集合
            // AsyncCallback callback,   异步回调
            // object asyncState
            session.BeginRead(
                new RequestHeader(),
                0,
                TimestampsToReturn.Both, // 返回结果的时间戳 (Both:服务端和客户端的时间戳)TimestampsToReturn.Server(服务端), TimestampsToReturn.Source(客户端)
                readValueIds,
                callback: ReadCompleted,
                session
            );

        }

        /// <summary>
        /// 异步读取回调
        /// </summary>
        /// <param name="result"></param>
        private static void ReadCompleted(IAsyncResult result)
        {
            Session? session = result.AsyncState as Session;

            if (session != null)
            {
                session.EndRead(result, out DataValueCollection results, out DiagnosticInfoCollection diagnosticInfos);

                foreach (DataValue item in results)
                {
                    // item.WrappedValue.TypeInfo.ValueRank 结果为 -1：针对值结果，1：针对集合结果
                    if (item.WrappedValue.TypeInfo.ValueRank == -1) // 针对值结果
                    {
                        Console.WriteLine(item.Value);
                    }
                    else if (item.WrappedValue.TypeInfo.ValueRank == 1)  // 针对集合结果
                    {
                        if (item.WrappedValue.TypeInfo.BuiltInType == BuiltInType.UInt16)
                        {
                            Console.WriteLine("数据类型示例.16 位设备.R 寄存器.WordArray:");
                            ushort[] us = (ushort[])item.Value;
                            foreach (ushort u in us)
                            {
                                Console.WriteLine(u);
                            }
                        }
                    }
                }
            }

        }

        /// <summary>
        /// 异步读取委托回调返回值
        /// </summary>
        /// <param name="session"></param>
        /// <param name="nodes"></param>
        /// <param name="callBack"></param>
        private static void UAAsyncRead(Session session, List<RequestNode> nodes, Action cb)
        {
            ReadValueIdCollection readValueIds = new ReadValueIdCollection();
            foreach (RequestNode node in nodes)
            {
                readValueIds.Add(new ReadValueId()
                {
                    NodeId = node.NodeId,
                    AttributeId = Attributes.Value,
                });
            }

            //RequestHeader requestHeader,  // 请求头
            //double maxAge,    // 毫秒,1000代表1秒钟前的数据,设置为0表示当前数据
            //TimestampsToReturn timestampsToReturn,  // 返回结果 (两个：服务端/客户端) 的时间戳
            //ReadValueIdCollection nodesToRead,      // 需要请求的Node集合
            // AsyncCallback callback,   异步回调
            // object asyncState
            session.BeginRead(
                new RequestHeader(),
                0,
                TimestampsToReturn.Both, // 返回结果的时间戳 (Both:服务端和客户端的时间戳)TimestampsToReturn.Server(服务端), TimestampsToReturn.Source(客户端)
                readValueIds,
                callback: (result) =>
                {
                    session.EndRead(result, out DataValueCollection results, out DiagnosticInfoCollection diagnosticInfos);

                    for (int i = 0; i < nodes.Count; i++)
                    {
                        nodes[i].Value = results[i];
                    }

                    // 因为是异步,main函数中同步执行获取RequestNode的值可能获取不到,那么我们用委托做一个回调,保证调用该委托才是真正的执行完成。
                    cb();
                },
                null
            );
        }

        /// <summary>
        /// 异步读取Task返回值
        /// </summary>
        /// <param name="session"></param>
        /// <param name="nodes"></param>
        /// <param name="callBack"></param>
        private static async Task UAAsyncTaskRead(Session session, List<RequestNode> nodes)
        {
            ReadValueIdCollection readValueIds = new ReadValueIdCollection();
            foreach (RequestNode node in nodes)
            {
                readValueIds.Add(new ReadValueId()
                {
                    NodeId = node.NodeId,
                    AttributeId = Attributes.Value,
                });
            }

            // RequestHeader requestHeader,  // 请求头
            // double maxAge,    // 毫秒,1000代表1秒钟前的数据,设置为0表示当前数据
            // TimestampsToReturn timestampsToReturn,  // 返回结果 (两个：服务端/客户端) 的时间戳
            // ReadValueIdCollection nodesToRead,      // 需要请求的Node集合
            // CancellationToken                       // 取消令牌
            CancellationToken cancellationToken = new CancellationToken();
            ReadResponse result = await session.ReadAsync(new RequestHeader(), 0, TimestampsToReturn.Both, readValueIds, cancellationToken);

            for (int i = 0; i < nodes.Count; i++)
            {
                nodes[i].Value = result.Results[i];
            }
        }

        // 业务层面处理可以写一个类
        class RequestNode
        {
            public string NodeId { get; set; }
            public object Value { get; set; }
        }

        #endregion

        #region UA 订阅
        private static void Subscription(Session session)
        {
            Subscription subscription = new Subscription(session.DefaultSubscription);
            MonitoredItem item = new MonitoredItem();
            // ns=2;s=通道 1.设备 1.标记 2
            item.StartNodeId = new NodeId("ns=2;s=通道 1.设备 1.标记 2");
            item.Notification += Item_Nofification;
            subscription.AddItem(item);
            session.AddSubscription(subscription);
            // 把订阅项添加到服务器
            subscription.Create();
        }

        private static void Item_Nofification(MonitoredItem monitoredItem, MonitoredItemNotificationEventArgs e)
        {
            MonitoredItemNotification monitorItem = ((MonitoredItemNotification)e.NotificationValue);
            Console.WriteLine($"订阅节点:{monitoredItem.StartNodeId},值:{monitorItem.Value.WrappedValue}");
        }
        #endregion

        #region UA写
        /// <summary>
        /// 同步写入
        /// </summary>
        /// <param name="session"></param>
        private static void Write(Session session)
        {
            WriteValueCollection writeValues = new WriteValueCollection();
            WriteValue writeValue = new WriteValue();
            writeValue.NodeId = new NodeId("ns=2;s=通道 1.设备 1.标记 2");
            writeValue.AttributeId = Attributes.Value; // 写入的是一个值(除了值以外还有其他属性可以写入)
            writeValue.Value.Value = (UInt16)666; // 写入的值,注入数据类型必须和标记的数据类型一致,标记是word
            writeValues.Add(writeValue);

            session.Write(
                new RequestHeader(),  // 请求头
                writeValues,   // 写入值集合
                out StatusCodeCollection results,  // 状态结果集合
                out DiagnosticInfoCollection diagnosticInfos  // 诊断信息集合
             );

            // 输出写入状态结果
            foreach (StatusCode item in results)
            {
                Console.WriteLine(StatusCode.IsGood(item));
            }
        }

        /// <summary>
        /// 异步写入 BeginWrite
        /// </summary>
        /// <param name="session"></param>
        private static void AsyncWrite(Session session)
        {
            WriteValueCollection writeValues = new WriteValueCollection();
            WriteValue writeValue = new WriteValue();
            writeValue.NodeId = new NodeId("ns=2;s=通道 1.设备 1.标记 2");
            writeValue.AttributeId = Attributes.Value; // 写入的是一个值(除了值以外还有其他属性可以写入)
            writeValue.Value.Value = (UInt16)555; // 写入的值,注入数据类型必须和标记的数据类型一致,标记是word
            writeValues.Add(writeValue);

            session.BeginWrite(new RequestHeader(), writeValues, new AsyncCallback(WriteCompleted), session);
        }

        private static void WriteCompleted(IAsyncResult result)
        {
            Session session = result.AsyncState as Session;
            session!.EndWrite(result,
                out StatusCodeCollection results,
                out DiagnosticInfoCollection diagnosticInfos
                );

            foreach (StatusCode item in results)
            {
                // 输出写入状态结果
                Console.WriteLine(StatusCode.IsGood(item));
            }
        }

        /// <summary>
        /// 异步写入 WriteAsync
        /// </summary>
        /// <param name="session"></param>

        private static async Task AsyncTaskWrite(Session session)
        {
            WriteValueCollection writeValues = new WriteValueCollection();
            WriteValue writeValue = new WriteValue();
            writeValue.NodeId = new NodeId("ns=2;s=通道 1.设备 1.标记 2");
            writeValue.AttributeId = Attributes.Value; // 写入的是一个值(除了值以外还有其他属性可以写入)
            writeValue.Value.Value = (UInt16)799; // 写入的值,注入数据类型必须和标记的数据类型一致,标记是word
            writeValues.Add(writeValue);

            CancellationTokenSource cts = new CancellationTokenSource();
            WriteResponse response = await session.WriteAsync(new RequestHeader(), writeValues, cts.Token);

            foreach (StatusCode item in response.Results)
            {
                // 输出写入状态结果
                Console.WriteLine(StatusCode.IsGood(item));
            }
        }
        #endregion

       
    }
}
